Skip to main content
A backend is rustic_core’s abstraction layer for storing repository data. Backends enable rustic to work with diverse storage systems while maintaining the same repository format.

What is a Backend?

Backends provide a simple interface to storage:

Read Operations

  • List files by type
  • Read full files
  • Read partial data (ranges)
  • Check file sizes

Write Operations

  • Write files
  • Delete files
  • Create repository structure
All backends provide the same interface, so switching storage doesn’t require application changes.

Backend Traits

rustic_core defines two core traits:

ReadBackend

pub trait ReadBackend: Send + Sync {
    /// Get backend location string
    fn location(&self) -> String;
    
    /// List all files of a given type with sizes
    fn list_with_size(&self, tpe: FileType) -> RusticResult<Vec<(Id, u32)>>;
    
    /// Read entire file
    fn read_full(&self, tpe: FileType, id: &Id) -> RusticResult<Bytes>;
    
    /// Read partial file (offset + length)
    fn read_partial(
        &self,
        tpe: FileType,
        id: &Id,
        cacheable: bool,
        offset: u32,
        length: u32,
    ) -> RusticResult<Bytes>;
    
    // ... warm-up methods
}

WriteBackend

pub trait WriteBackend: ReadBackend {
    /// Create repository structure
    fn create(&self) -> RusticResult<()>;
    
    /// Write file data
    fn write_bytes(
        &self,
        tpe: FileType,
        id: &Id,
        cacheable: bool,
        buf: Bytes,
    ) -> RusticResult<()>;
    
    /// Remove file
    fn remove(
        &self,
        tpe: FileType,
        id: &Id,
        cacheable: bool,
    ) -> RusticResult<()>;
}
WriteBackend extends ReadBackend, so all backends that support writes also support reads.

Available Backends

rustic_core supports four backend types:
Local filesystem backend - Stores repository on local or mounted filesystems.
use rustic_backend::LocalBackend;

let backend = LocalBackend::new("/path/to/repository")?;
Features:
  • Direct filesystem access
  • No network overhead
  • Simple and fast
  • Works with NFS, SMB, etc.
Best for:
  • Local backups
  • Network-attached storage (NAS)
  • Testing and development

Choosing a Backend

Decision matrix for selecting the right backend:

Backend Comparison

BackendSetupDependenciesPerformanceProviders
LocalSimpleNoneFastestFilesystem
RESTMediumREST serverFastREST API
OpenDALMediumNoneFast10+ clouds
RcloneComplexrclone binaryGood40+ services

Backend Configuration

Configure backends using BackendOptions:
use rustic_backend::{BackendOptions, SupportedBackend};

let opts = BackendOptions {
    repository: Some("/backup/repo".to_string()),
    repo_hot: None,  // Optional hot repository
    ..Default::default()
};

let backend = opts.to_backend()?;

Hot/Cold Backend Setup

Combine fast and slow storage:
let opts = BackendOptions {
    repository: Some("s3://cold-archive/repo".to_string()),
    repo_hot: Some("/fast/ssd/cache".to_string()),
    ..Default::default()  
};

let backends = opts.to_backends()?;
let repo = Repository::new(&repo_opts, &backends)?;
See the Repository hot/cold architecture for details.

Backend Operations

File Types

Backends organize files by type:
pub enum FileType {
    Config,    // Repository configuration
    Index,     // Blob indexes
    Key,       // Encryption keys
    Snapshot,  // Snapshot metadata  
    Pack,      // Data packs
}
Each type is stored in a separate directory:
repository/
├── config
├── keys/
├── snapshots/
├── index/
└── data/

Listing Files

// List all snapshots
let snapshots = backend.list(FileType::Snapshot)?;

for id in snapshots {
    println!("Snapshot: {}", id);
}

// List with sizes
let packs = backend.list_with_size(FileType::Pack)?;

for (id, size) in packs {
    println!("Pack {}: {} bytes", id, size);
}

Reading Data

// Read entire file
let data = backend.read_full(FileType::Snapshot, &snapshot_id)?;

// Read partial (for large pack files)
let chunk = backend.read_partial(
    FileType::Pack,
    &pack_id,
    true,      // cacheable
    offset,
    length,
)?;

Writing Data

use bytes::Bytes;

let data = Bytes::from("snapshot data");

backend.write_bytes(
    FileType::Snapshot,
    &snapshot_id,
    true,  // cacheable
    data,
)?;

Caching

rustic_core includes a sophisticated caching layer:

What Gets Cached?

Small, frequently accessed files:
  • Snapshots
  • Index files
  • Tree blobs
  • Config file
These are marked cacheable: true.

Cache Location

Default cache locations:
PlatformDefault Cache Directory
Linux~/.cache/rustic/
macOS~/Library/Caches/rustic/
Windows%LOCALAPPDATA%\rustic\cache\
Override with:
let opts = RepositoryOptions {
    cache_dir: Some("/custom/cache".into()),
    ..Default::default()
};
Or disable:
let opts = RepositoryOptions {
    no_cache: true,
    ..Default::default()  
};

Warm-Up Support

Some backends (especially cold storage) support warm-up to pre-fetch data:
// Check if warm-up is needed
if backend.needs_warm_up() {
    // Trigger warm-up for pack file
    backend.warm_up(FileType::Pack, &pack_id)?;
}

Warm-Up Commands

Configure custom warm-up commands:
let opts = RepositoryOptions {
    warm_up_command: Some(CommandInput::from_str(
        "glacier restore --archive-id %id"
    )?),
    warm_up_wait: Some(Duration::from_secs(3600)),  // Wait 1 hour
    ..Default::default()
};
Warm-up is useful for:
  • Glacier/Archive tier storage
  • Tape archives
  • Tiered cloud storage

Error Handling

Backends return typed errors:
match backend.read_full(FileType::Snapshot, &id) {
    Ok(data) => process_snapshot(data),
    Err(e) if e.kind() == ErrorKind::NotFound => {
        println!("Snapshot not found");
    }
    Err(e) if e.kind() == ErrorKind::Network => {
        println!("Network error, retrying...");
        retry_with_backoff()?;
    }
    Err(e) => return Err(e),
}

Backend Features

Enable backends via Cargo features:
[dependencies]
rustic_backend = { version = "0.1", features = ["opendal", "rest"] }

# Or enable all
rustic_backend = { version = "0.1", features = ["cli"] }
Available features:
  • opendal - OpenDAL backend (default)
  • rest - REST backend (default)
  • rclone - Rclone backend (default)
  • cli - Command-line support

Advanced: Custom Backends

Implement custom backends by implementing the traits:
use rustic_core::backend::{ReadBackend, WriteBackend};

struct MyBackend {
    // Your storage implementation
}

impl ReadBackend for MyBackend {
    fn location(&self) -> String {
        "my-custom-backend".to_string()
    }
    
    fn list_with_size(&self, tpe: FileType) -> RusticResult<Vec<(Id, u32)>> {
        // Your implementation
    }
    
    // ... implement other methods
}

impl WriteBackend for MyBackend {
    fn create(&self) -> RusticResult<()> {
        // Create repository structure
    }
    
    // ... implement write methods
}
Custom backends enable:
  • Integration with proprietary storage
  • Custom caching strategies
  • Specialized optimizations
  • Compliance requirements

Backend Best Practices

Reuse backend instances for better performance:
// Good: Reuse backend
let backend = Arc::new(backend);
for file in files {
    backend.write_bytes(tpe, &id, true, data)?;
}

// Bad: Create new backend each time
for file in files {
    let backend = LocalBackend::new(path)?;
    backend.write_bytes(tpe, &id, true, data)?;
}
Implement exponential backoff for transient failures:
let mut retries = 0;
let max_retries = 3;

loop {
    match backend.read_full(tpe, &id) {
        Ok(data) => break Ok(data),
        Err(e) if retries < max_retries && e.is_transient() => {
            retries += 1;
            sleep(Duration::from_secs(2_u64.pow(retries)));
        }
        Err(e) => break Err(e),
    }
}
Backends are thread-safe (Send + Sync):
use rayon::prelude::*;

let backend = Arc::new(backend);

file_ids.par_iter().try_for_each(|id| {
    let data = backend.read_full(FileType::Pack, id)?;
    process_data(data)
})?;

See Also

Repository

How backends store repository structure

Encryption

Data encryption before backend storage

Deduplication

How backends store deduplicated packs

Snapshots

Snapshot metadata in backend storage